#!/usr/bin/env escript
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil -*-
%%
%% Helper to generate code for eval dispatch.
%% Could be extended with fetching and writing arguments
%% and checking types in the future.
%%
-mode(compile).

main(Args) ->
    {ShowHelp, OutFile} = process_args(Args),
    maybe_show_help(ShowHelp),
    do_main(OutFile).

usage() ->
    io:format("Usage: aefa_gen_dispatch [--help | OUT_FILE]~n", []).

process_args(["--help" | _]) ->
    {true, nil};
process_args([OutFile]) ->
    {false, OutFile};
process_args(_) ->
    {true, nil}.

maybe_show_help(true) ->
    usage(),
    halt(0);
maybe_show_help(false) ->
    ok.

-define(IS_DEBUG_OP(Op),
    (Op =:= dbg_def orelse
     Op =:= dbg_undef orelse
     Op =:= dbg_loc orelse
     Op =:= dbg_contract)).

do_main(Filename) ->
    {ok, File} = file:open(Filename, [write]),
    Ops = aeb_fate_generate_ops:get_ops(),
    Instructions = lists:flatten([gen_eval(Op, eval) || Op <- Ops]),
    DbgInstructions = lists:flatten([gen_eval(Op, eval_dbg) || Op <- Ops]),
    io:format(File,
	      "%%\n%% This file is generated. Any modifications will be overwritten.\n%%\n"
	      "-module(aefa_fate_eval).\n\n"
	      "-export([eval/2]).\n\n"
              "~s"
              "eval(Op, _EngineState) ->\n"
              "    throw({error, unknown_op, Op}).\n\n"
              "-ifdef(DEBUG_INFO).\n"
              "~s"
              "eval_dbg(Op, _EngineState) ->\n"
              "    throw({error, unknown_op, Op}).\n"
              "-else.\n"
              "eval_dbg(Op, _EngineState) ->\n"
              "    throw({error, unknown_op, Op}).\n"
              "-endif.\n"
             , [Instructions, DbgInstructions]),
    io:format(File, "\n", []),
    file:close(File).

gen_eval(#{ constructor := Constructor, gas := Gas } = Op, EvalKind) ->
    check_descending(Constructor, Gas),
    Cmd = io_lib:format("aefa_fate_op:~w(~saefa_engine_state:spend_gas(~w, EngineState))",
                        [Constructor, gen_cargs(Op), Gas]),
    Body =
        case Constructor of
            C when ?IS_DEBUG_OP(C) andalso EvalKind =:= eval ->
                io_lib:format("    eval_dbg(~s, EngineState)", [gen_op_w_args(Op)]);
            _ ->
                [ gen_allowed_offchain(Op)
                , gen_in_auth(Op, gen_next_cmd(Op, Cmd)) ]
        end,
    PrintFun = EvalKind =:= eval orelse ?IS_DEBUG_OP(Constructor),
    case PrintFun of
        true ->
            io_lib:format("~s(~s, EngineState) ->\n~s;\n\n",
                        [atom_to_list(EvalKind), gen_op_w_args(Op), Body]);
        false ->
            ""
    end.

gen_next_cmd(#{ end_bb := true },  Cmd) -> Cmd;
gen_next_cmd(#{ end_bb := false }, Cmd) -> io_lib:format("{next, ~s}", [Cmd]).

gen_in_auth(#{ in_auth := false, opname := Name }, Cmd) ->
    io_lib:format(
      "    case aefa_engine_state:in_auth_context(EngineState) of\n"
      "        false -> ~s;\n"
      "        true  -> aefa_fate:abort({not_allowed_in_auth_context, ~w}, EngineState)\n"
      "    end", [Cmd, Name]);
gen_in_auth(_Op, Cmd) ->
    ["    ", Cmd].

gen_allowed_offchain(#{ offchain := false, opname := Name }) ->
    io_lib:format(
      "    case aefa_engine_state:is_onchain(EngineState) of\n"
      "        true  -> ok;\n"
      "        false -> aefa_fate:abort({not_allowed_offchain, ~w}, EngineState)\n"
      "    end,\n", [Name]);
gen_allowed_offchain(#{ offchain := true}) ->
    "".

gen_op_w_args(#{ opname := Name, format := [] }) ->
    io_lib:format("~w", [Name]);
gen_op_w_args(#{ opname := Name, format := Fmt }) ->
    io_lib:format("{~w~s}", [Name, gen_arg_matches(Fmt, 0)]).

gen_cargs(#{ arity := Arity }) ->
    [ io_lib:format("Arg~w, ", [N]) || N <- lists:seq(0, Arity-1) ].

gen_arg_matches([], _) ->
    "";
gen_arg_matches([ a | Rest], N) ->
    io_lib:format(", Arg~w", [N]) ++ gen_arg_matches(Rest, N+1);
gen_arg_matches([is | Rest], N) ->
    io_lib:format(", {immediate, Arg~w}", [N]) ++ gen_arg_matches(Rest, N+1);
gen_arg_matches([li | Rest], N) ->
    io_lib:format(", {immediate, Arg~w}", [N]) ++ gen_arg_matches(Rest, N+1);
gen_arg_matches([ii | Rest], N) ->
    io_lib:format(", {immediate, Arg~w}", [N]) ++ gen_arg_matches(Rest, N+1).

check_descending(_Op, X) when is_integer(X) -> ok;
check_descending(_Op, [_]) -> ok;
check_descending(Op, [{P1, _}, {P2, _} = G | Rest]) when P2 < P1 ->
    check_descending(Op, [G | Rest]);
check_descending(Op, _) ->
    error({Op, gas_options_not_descending}).
